/*
 * @(#)AbstractJdbcProvider.java 1.0 2000/12/14
 * 
 * Copyright 2000 Thomas Barnekow. All Rights Reserved.
 * 
 * This software is the proprietary information of Thomas Barnekow. Use is
 * subject to license terms.
 */

package tb.provider.jdbc;

import java.sql.*;
import java.util.*;

import tb.jots.*;
import tb.provider.*;
import tb.util.*;

/**
 * This class is an abstract base class for JDBC service providers leveraging
 * the Java Object Transaction Service (JOTS). JDBC service providers extending
 * this class can operate in different modes:
 * <p>
 * In <i>shared</i> mode a single database connection is used by all client
 * threads. In <i>unshared</i> mode, however, each thread uses its own
 * specific connection. In <i>auto-connect</i> mode connections are
 * established on-demand. Otherwise, the service provider has to be initialized
 * with a connection upon instantiation. In <i>auto-disconnect</i> mode
 * connections are released after each database operation/transaction.
 * Otherwise, connections have to be explicitly (or implicitly, during garbage
 * collection) closed.
 * <p>
 * The <i>auto-disconnect</i> mode can only be used in conjunction with the
 * <i>unshared</i> mode. The latter entails the <i>auto-connect</i> mode.
 * 
 * @author Thomas Barnekow
 * @version 1.0
 */
public abstract class AbstractJdbcProvider
	extends AbstractProvider
	implements Resource {

	/**
	 * Database URL, e.g., jdbc:odbc:scott.
	 */
	private String m_url;

	/**
	 * Database user's name.
	 */
	private String m_user;

	/**
	 * Database user's password.
	 */
	private String m_password;

	/**
	 * The <i>sharedMode</i> flag tells us whether this JDBC service provider
	 * is using a single shared connection (shared mode) or multiple
	 * thread-specific connections (unshared mode, one connection per thread).
	 */
	private boolean m_sharedMode = true;

	/**
	 * Automatically open connection using connection parameters?
	 */
	private boolean m_autoConnectMode = false;

	/**
	 * Automatically close connection after each database operation?
	 */
	private boolean m_autoDisconnectMode = false;

	/**
	 * Connection map.
	 */
	private Map m_connectionMap =
		Collections.synchronizedMap(new WeakHashMap());

	/**
	 * Shared context.
	 */
	private final Object m_sharedConnectionContext = new Object();

	/**
	 * Delegate supporting the implementation of {@link tb.jots.Resource}s.
	 */
	private ResourceSupport m_resourceSupport = new ResourceSupport(this);

	/**
	 * Create a JDBC service provider operating in <i>shared</i> mode.
	 * 
	 * @param name
	 *            the service provider's name
	 * @param connection
	 *            JDBC connection shared by all threads
	 */
	public AbstractJdbcProvider(QualifiedName name, Connection connection) {
		super(name);

		m_sharedMode = true;
		m_autoConnectMode = false;
		m_autoDisconnectMode = false;

		if (connection == null) {
			throw new NullPointerException("Connection must not be null");
		}
		setConnection(connection);
	}

	/**
	 * Create a JDBC service provider operating in <i>unshared auto-connect
	 * </i> mode.
	 * 
	 * @param name
	 *            the service provider's name
	 * @param url
	 *            database URL
	 * @param user
	 *            database user's name
	 * @param password
	 *            database user's password
	 */
	public AbstractJdbcProvider(
		QualifiedName name,
		String url,
		String user,
		String password) {
		this(name, url, user, password, false);
	}

	/**
	 * Create a JDBC service provider operating in <i>auto-connect</i> mode.
	 * 
	 * @param name
	 *            the service provider's name
	 * @param url
	 *            database URL
	 * @param user
	 *            database user's name
	 * @param password
	 *            database user's password
	 * @param sharedMode
	 *            true = share a single connection, false = use thread-specific
	 *            connections.
	 */
	public AbstractJdbcProvider(
		QualifiedName name,
		String url,
		String user,
		String password,
		boolean sharedMode) {
		super(name);

		if (url == null) {
			throw new IllegalArgumentException("Database URL must not be null");
		}

		m_url = url;
		m_user = user;
		m_password = password;

		m_sharedMode = sharedMode;
		m_autoConnectMode = true;
		m_autoDisconnectMode = false;
	}

	/*
	 * Properties
	 */

	/**
	 * Get the database URL.
	 */
	public final String getURL() {
		return m_url;
	}

	/**
	 * Get the database user's login name.
	 */
	public final String getUser() {
		return m_user;
	}

	/**
	 * Get the database user's login password.
	 */
	public final String getPassword() {
		return m_password;
	}

	/**
	 * The <i>sharedMode</i> flag tells us whether this JDBC service provider
	 * is using a single shared connection (shared mode) or multiple
	 * thread-specific connections (unshared mode, one connection per thread).
	 * 
	 * @return true = shared mode, false = unshared mode
	 */
	public final boolean isSharedMode() {
		return m_sharedMode;
	}

	/**
	 * The <i>autoConnectMode</i> flag tells us whether or not the connection
	 * will be automatically established.
	 */
	public final boolean isAutoConnectMode() {
		return m_autoConnectMode;
	}

	/**
	 * In shared mode, the <i>autoDisconnectMode</i> shows whether or not the
	 * thread-specific connections will be automatically closed after each
	 * database operation.
	 */
	public final boolean isAutoDisconnectMode() {
		return m_autoDisconnectMode;
	}

	/**
	 * When in unshared mode, set or reset the <i>autoDisconnectMode</i>
	 * flag. In shared mode, this has no effect.
	 * 
	 * @param value
	 *            true: autoDisconnectMode = ON, false: autoDisconnectMode =
	 *            OFF.
	 */
	public final void setAutoDisconnectMode(boolean value) {
		if (!m_sharedMode)
			m_autoDisconnectMode = value;
	}

	/**
	 * Get the supporting delegate.
	 */
	protected final ResourceSupport getResourceSupport() {
		return m_resourceSupport;
	}

	/*
	 * Connection management methods
	 */

	/**
	 * Get connection context.
	 * 
	 * @return shared or unshared connection context
	 * @see #getUnsharedConnectionContext
	 */
	protected Object getConnectionContext() {
		if (m_sharedMode)
			return m_sharedConnectionContext;
		else
			return getUnsharedConnectionContext();
	}

	/**
	 * Get the context for an unshared connection. The default context is the
	 * current {@link Thread}. Override this method in order to return, e.g.,
	 * a {@link tb.jots.Control}object reference.
	 */
	protected Object getUnsharedConnectionContext() {
		return Thread.currentThread();
	}

	/**
	 * Get the given <code>context</code>'s connection.
	 * 
	 * @param context
	 *            e.g., the current {@link Thread}or {@link tb.jots.Control}
	 * @return the {@link Connection}, which may be <code>null</code>
	 */
	protected final Connection getConnection(Object context) {
		return (Connection) m_connectionMap.get(context);
	}

	/**
	 * Store the given <code>connection</code> for later retrieval. This
	 * provider only maintains a weak reference to the given <code>context</code>
	 * Object. That is, the connection object will be garbage collected,
	 * whereby the connection is automatically closed, when the <code>context</code>
	 * Object is no longer referenced by any object other than this provider.
	 */
	protected final Connection putConnection(
		Object context,
		Connection connection) {
		if (context == null || connection == null) {
			throw new NullPointerException();
		}
		return (Connection) m_connectionMap.put(context, connection);
	}

	/**
	 * Remove the mapping context -> connection.
	 */
	protected final Connection removeConnection(Object context) {
		return (Connection) m_connectionMap.remove(context);
	}

	/**
	 * Get or create a {@link Connection}.
	 * 
	 * @return the {@link Connection}for the current context
	 * @exception SQLException
	 *                in case the connection can't be established
	 */
	public final Connection getConnection() throws SQLException {
		Object context = getConnectionContext();
		Connection connection = null;

		synchronized (context) {
			if ((connection = getConnection(context)) == null
				&& m_autoConnectMode) {
				connection =
					DriverManager.getConnection(m_url, m_user, m_password);
				connection.setAutoCommit(false);
				putConnection(context, connection);
			}
		}

		// Done
		return connection;
	}

	/**
	 * Set <code>connection</code> for current context.
	 * 
	 * @see #getConnectionContext
	 */
	protected final void setConnection(Connection connection) {
		putConnection(getConnectionContext(), connection);
	}

	private final void _closeConnection(Object context) {
		Connection connection = getConnection(context);
		try {
			if (connection != null && !connection.isClosed()) {
				connection.close();
			}
		} catch (SQLException ex) {
			ex.printStackTrace(System.err);
		}
	}

	protected final void closeConnection(Object context) {
		_closeConnection(context);
		removeConnection(context);
	}

	/**
	 * Close the current context's {@link Connection}.
	 */
	public final void closeConnection() {
		closeConnection(getConnectionContext());
	}

	/**
	 * Close all contexts' {@link Connection}s.
	 */
	public final void closeAllConnections() {
		Iterator iterator = m_connectionMap.keySet().iterator();
		while (iterator.hasNext()) {
			_closeConnection(iterator.next());
			iterator.remove();
		}
	}

	/**
	 * In <i>unshared auto-disconnect</i> mode the current thread's
	 * connection will be closed. This method should be called after each
	 * database operation/transaction in order to support the <i>
	 * auto-disconnect</i> mode.
	 */
	protected final void releaseConnection() {
		if (!m_sharedMode && m_autoDisconnectMode)
			closeConnection();
	}

	/**
	 * Execute an insert, update, or delete statement.
	 * 
	 * @param query
	 *            SQL insert, update, or delete statement
	 * @return the number of rows affected by this update
	 * @exception SQLException
	 *                in case of a database error
	 * @exception JotsException
	 *                in case of a JOTS error
	 */
	protected int executeUpdate(String query)
		throws SQLException, JotsException {
		// Require a transaction (context).
		m_resourceSupport.requireTransaction();

		// Get the context's (e.g., Thread or Control instance) connection
		Connection connection = getConnection();
		Statement stmt = null;
		int rowCount = 0;

		try {
			synchronized (connection) {
				stmt = connection.createStatement();
				rowCount = stmt.executeUpdate(query);
			}
			m_resourceSupport.voteCommit();
		} catch (SQLException ex) {
			m_resourceSupport.rollbackOnly();
			throw ex;
		} finally {
			try {
				if (stmt != null)
					stmt.close();
			} catch (SQLException ex) {
				ex.printStackTrace(System.err);
			}
			releaseConnection();
		}

		return rowCount;
	}

	/*
	 * Resource operations
	 */

	public int prepare() {
		return m_resourceSupport.getVote();
	}

	public void rollback()
		throws
			HeuristicCommitException,
			HeuristicMixedException,
			HeuristicHazardException {
		Connection connection = null;
		try {
			if ((connection = getConnection()) == null) {
				throw new HeuristicHazardException("Not connected");
			}
			connection.rollback();
		} catch (SQLException ex) {
			throw new HeuristicHazardException("Driver threw an exception", ex);
		}
	}

	public void commit()
		throws
			NotPreparedException,
			HeuristicRollbackException,
			HeuristicMixedException,
			HeuristicHazardException {
		try {
			commitOnePhase();
		} catch (TransactionRolledBackException ex) {
			throw new HeuristicRollbackException(ex.getMessage(), ex);
		}
	}

	public void commitOnePhase()
		throws TransactionRolledBackException, HeuristicHazardException {
		Connection connection = null;
		try {
			if ((connection = getConnection()) == null)
				throw new HeuristicHazardException("Not connected");
		} catch (SQLException ex) {
			throw new HeuristicHazardException("Driver threw an exception", ex);
		}

		try {
			connection.commit();
		} catch (SQLException cex) {
			try {
				connection.rollback();
			} catch (SQLException rex) {
				throw new HeuristicHazardException(
					"Driver threw an exception",
					rex);
			}
			throw new TransactionRolledBackException(
				"Driver threw an exception",
				cex);
		}
	}

	public void forget() {
		releaseConnection();
	}
}
